package sim;

import java.awt.geom.Point2D;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import model.GameConstants;
import model.IModel;
import model.commands.AcceleratePlayerCommand;
import model.commands.SpinPlayerCommand;
import network.IMessageListener;
import network.NetworkAdapter;
import sim.states.AbstractState;
import sim.states.NewRoundState;
import sim.states.PlayState;
import sim.states.RequestSideState;
import sim.states.StartState;
import sim.states.VersionState;
import ctf.Log;
import ctf.model.Side;
import ctf.network.ProtocolError;
import ctf.network.SocketClosedException;
import ctf.network.UninitializedSocketException;

/**
 * The class used to govern the over all interaction of a controller with the simulator.
 * @author N. Bhatia, A. Butt, N. Evans, C. Levesque, P. OKeefe
 *
 */
public class Simulator implements IMessageListener, ISimulator {

	private NetworkAdapter networkAdapter;
	private ArrayList<Integer> clientIDList;
	private Map<Integer, Side> clientSideMap;
	private Map<Integer, State> clientStates;
	private Map<State, AbstractState> states;
	private IModel model;
	private volatile Thread modelRefresher;

	public Simulator(final IModel model) {
		//Log.getInstance().setOutputStream(new Console("Capture the Fatty"));
		Log.getInstance().println("Welcome to Capture the Fatty");
		Log.getInstance().printRow();
		Log.getInstance().println(
						"A game by C.Levesque, A.Butt, P.O'Keefe, N.Evans, and N.Bhatia");
		Log.getInstance().println(
				"Produced for ENGI7893, Software Engineering - D.Peters");
		Log.getInstance().printRow();
		Log.getInstance().println("Initializing system.");
		this.model = model;
		new StartState(this);
		clientIDList = new ArrayList<Integer>();
		clientSideMap = new HashMap<Integer, Side>();
		clientStates = new HashMap<Integer, State>();
		states = new HashMap<State, AbstractState>();
		states.put(State.START, new StartState(this));
		states.put(State.VERSION, new VersionState(this));
		states.put(State.REQUEST_SIDE, new RequestSideState(this));
		states.put(State.NEW_ROUND, new NewRoundState(this));
		states.put(State.PLAY, new PlayState(this));

		try {
			this.networkAdapter = new NetworkAdapter(2, this);
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UninitializedSocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	@Override
	public void messageReceived(int clientID, String message)
			throws ProtocolError {
		//Log.getInstance().println("<client#" + clientID + "> " + message);
		State currentState = clientStates.get(clientID);
		states.get(currentState).request(clientID, message);
	}

	@Override
	public Collection<Point2D> getTrees() {
		return this.model.trees();
	}

	@Override
	public void placeFlag(int clientId, Point2D position) throws ProtocolError {
		Side side = clientSideMap.get(clientId);
		this.model.setFlagPosition(side, position);

	}

	@Override
	public void respond(int clientID, String message) {
		try {
			networkAdapter.sendMessage(message, clientID);
		} catch (SocketClosedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UninitializedSocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	@Override
	public void setState(int clientID, State state) {
		clientStates.put(clientID, state);
	}

	@Override
	public void registerClient(int clientID) {
		clientIDList.add(clientID);
		clientStates.put(clientID, State.START);
	}

	@Override
	public void setTeamSide(int clientId, Side side) {
		clientSideMap.put(clientId, side);
	}

	@Override
	public void acceleratePlayer(int clientId, int playerNum, int delayInMs,
			float newAcceleration) throws ProtocolError {
		Side side = clientSideMap.get(clientId);
		this.model.addCommand(new AcceleratePlayerCommand(
				this.model.getTime() + delayInMs, side, playerNum, newAcceleration
				));
	}

	@Override
	public void placePlayer(int clientId, int playerNum, Point2D position) {
		Side side = clientSideMap.get(clientId);
		this.model.placePlayer(side, playerNum, position);
	}

	@Override
	public void setTime(int clientId, int newTimeInMs) {
		// Side side = clientSideMap.get(clientId);
		this.model.setTime(newTimeInMs);
	}

	@Override
	public int getTime() {
		return this.model.getTime();
	}

	@Override
	public void spinPlayer(int clientId, int playerNum, int delayInMs,
			float newAngularVelocity) throws ProtocolError {
		Side side = clientSideMap.get(clientId);
		this.model.addCommand(new SpinPlayerCommand(
				this.model.getTime() + delayInMs, side, playerNum, newAngularVelocity
				));

	}

	@Override
	public void pause() {
		modelRefresher = null;
	}

	@Override
	public void play() {
		modelRefresher = new AutoRefresher();
		modelRefresher.start();
	}

	@Override
	public void reset() {
		this.pause();
		this.model.reset();
		this.clientIDList.clear();
		this.networkAdapter.reset();
		Collection<AbstractState> listStates = states.values();
		Iterator<AbstractState> iter = listStates.iterator();
		while (iter.hasNext()) {
			iter.next().reset();
		}
	}

	@Override
	public void resetListener() {
		this.reset();
	}

	@Override
	public String getGameState(int clientId) {
		Side side = this.clientSideMap.get(clientId);
		String msg =  this.model.publish(side);
		return msg;
	}

	class AutoRefresher extends Thread implements Runnable{
       public void run() {
        	long lastTime = System.currentTimeMillis();
            while (modelRefresher == Thread.currentThread()){
                waiting(GameConstants.MODEL_UPDATE_PERIOD);
                long currentTime = System.currentTimeMillis(); 
                long deltaTime = currentTime - lastTime;
                model.update((int)deltaTime);
                lastTime = currentTime;
            }
        }
        
        private void waiting(long ms){
            long thetime = System.currentTimeMillis();
            boolean blnrepeat = true;
            while (blnrepeat){
                blnrepeat = (thetime + ms) > System.currentTimeMillis();
            }
        }
	}
}
